跳到主要内容

第11章指针基础与串口实用程序

指针是C语言最核心的部分,而UART串口通信是单片机最常用的一种通信方式。因此这两部分内容,除了在第10章进行简单的介绍外,本章还需要进一步加深学习,用实用的例子来不断增强对于这两部分内容的理解和应用能力。

11.1指向数组元素的指针

11.1.1指向数组元素的指针和运算法则

所谓指向数组元素的指针,其本质还是变量的指针。因为数组中的每个元素,其实都可以直接看成是一个变量,所以指向数组元素的指针,也就是变量的指针。 指向数组元素的指针不难,但很常用。

unsigned char number[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
unsigned char *p;

如果写p = &number[0];那么指针p就指向了number的第0号元素,也就是把number[0]的地址赋值给了p,同理,如果写p = &number[1];p就指向了数组number的第1号元素。p = &number[x];其中x的取值范围是0~9,就表示p指向了数组number的第x号元素。

指针本身,也可以进行几种简单的运算,这几种运算对于数组元素的指针来说应用最多。

  1. 比较运算。比较的前提是两个指针指向同种类型的对象,比如两个指针变量p和q它们指向了具有同种数据类型的数组,那它们可以进行<>>=<===等关系运算。如果p==q为真的话,表示这两个指针指向的是同一个元素。
  2. 指针和整数可以直接进行加减运算。比如还是前边讲的指针p和数组number,如果p = &number[0],那么p+1就指向了number[1]p+9就指向了number[9]。当然了,如果p = &number[9]p-9也就指向了number[0]
  3. 两个指针变量在一定条件下可以进行减法运算。如p = &number[0]; q = &number[9];那么q-p的结果就是9。这个地方要特别注意,这个9代表的是元素的个数,而不是真正的地址差值。如果number的变量类型是unsigned int型,占2个字节,q-p的结果依然是9,因为它代表的是数组元素的个数。

在数组元素指针这里还有一种情况,数组名字就代表了数组元素的首地址,也就是说:

p = &number[0];
p = number;

这两种表达方式是等价的,因此以下几种表达形式和内容需要格外注意一下。

根据指针的运算规则,p+x代表的是number[x]的地址,那么number+x代表的也是number[x]的地址。或者说,它们指向的都是number数组的第x号元素。

*(p+x)和*(number+x)都表示number[x]

指向数组元素的指针也可以表示成数组的形式,也就是说,允许指针变量带下标,即p[i]*(p+i)是等价的。为了避免混淆,这里建议不要写成前者,而一律采用后者的写法。

二维数组元素的指针和一维数组类似,需要介绍的内容不多。假如现在一个指针变量p和一个二维数组number[3][4],它的地址的表达方式也就是p=&number[0][0],有一个地方要注意,既然数组名代表了数组元素的首地址,那么也就是说p和number都是指数组的首地址。对二维数组来说,number[0]number[1]number[2]都可以看成是一维数组的数组名字,所以number[0]等价于&number[0][0]number[1]等价于&number[1][0]number[2]等价于&number[2][0]。加减运算和一维数组是类似的,不再详述。

11.1.2指向数组元素指针的实例

在C语言里边,sizeof()可用来获取括号内对象所占用的字节数,虽然写成函数形式,但它不是一个函数,而是C语言的一个关键字,sizeof()在程序中相当于一个常量,也就是说这个获取操作是在程序编译的时候进行的,而不是在程序运行的时候进行。这是一个实际编程中很有用的关键字,灵活运用它可以为程序带来更好的可读性、易维护性和可移植性。

sizeof()括号中可以是变量名,也可以是变量类型名。而其更大的用处是与数组名搭配使用,可以获取整个数组占用的字节数,不用自己动手计算了,可以避免错误,而如果日后改变了数组的维数时,也不需要执行代码中逐个修改,便于程序的维护和移植。

下面提供一个简单的串口演示例程,可以体验一下指针和sizeof()的用法。例程首先接收上位机下发的命令,根据命令值分别把不同数组的数据回发给上位机,程序还用到了指针的自增运算,也就是+1运算,体会一下指针ptrTxd在串口发送的过程中的指向是如何变化的。在上位机串口调试助手中分别下发1、2、3、4,就会得到不同的数组回发,注意这里都用十六进制发送和十六进制显示。

此外,前边讲了串口发送中断标志位TI是硬件置位,软件清零的。通常来讲,如果想一次发送多个数据的时候,就需要把第一个字节写入SBUF,然后再等待发送中断,在后续中断中再发送剩余的数据,这样数据发送过程就被拆分到了两个地方——主循环内和中断服务函数内,无疑就使得程序结构变得零散了。这个时候,为了使程序结构尽量紧凑,在启动发送的时候,不是向SBUF中写入第一个待发的字节,而是直接让TI=1,注意,这时候会马上进入串口中断,因为中断标志位置1了,但是串口线上并没有发送任何数据。于是,所有的数据发送都可以在中断中进行,而不用再分为两部分了。

#include <reg52.h>

bit cmdArrived = 0; //命令到达标志,即接收到上位机下发的命令
unsigned char cmdIndex = 0; //命令索引,即与上位机约定好的数组编号
unsigned char cntTxd = 0; //串口发送计数器
unsigned char *ptrTxd; //串口发送指针

unsigned char array1[1] = {1};
unsigned char array2[2] = {1,2};
unsigned char array3[4] = {1,2,3,4};
unsigned char array4[8] = {1,2,3,4,5,6,7,8};

void ConfigUART(unsigned int baud);

void main()
{
EA = 1; //开总中断
ConfigUART(9600); //配置波特率为9600

while (1)
{
if (cmdArrived)
{
cmdArrived = 0;
switch (cmdIndex)
{
case 1:
ptrTxd = array1; //数组1的首地址赋值给发送指针
cntTxd = sizeof(array1); //数组1的长度赋值给发送计数器
TI = 1; //手动方式启动发送中断,处理数据发送
break;
case 2:
ptrTxd = array2;
cntTxd = sizeof(array2);
TI = 1;
break;
case 3:
ptrTxd = array3;
cntTxd = sizeof(array3);
TI = 1;
break;
case 4:
ptrTxd = array4;
cntTxd = sizeof(array4);
TI = 1;
break;
default:
break;
}
}
}
}
/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud; //计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}
/* UART中断服务函数 */
void InterruptUART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //清零接收中断标志位
cmdIndex = SBUF; //接收到的数据保存到命令索引中
cmdArrived = 1; //设置命令到达标志
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
if (cntTxd > 0) //有待发送数据时,继续发送后续字节
{
SBUF = *ptrTxd; //发出指针指向的数据
cntTxd--; //发送计数器递减
ptrTxd++; //发送指针递增
}
}
}

采用逻辑分析仪将4次收发数据全部抓出来,直观的做一下对比,如图11-1所示。

图11-1 逻辑分析仪抓取串口数据数据

11.2字符数组和字符指针

11.2.1常量和符号常量

在程序运行过程中,其值不能被改变的量称之为常量。常量分为不同的类型,有整型常量如123100;浮点型常量3.140.56-4.8;字符型常量‘a’、‘b’、‘0’;字符串常量“a”、 “abc”、“1234”、“1234abcd”等。

整型和浮点型常量直接写的数字,字符型常量用单引号来表示一个字符,用双引号来表示一个字符串,尤其要注意‘a’和“a”是不一样的,后边会详细介绍。

常量一般有两种表现形式。

直接常量:直接以值的形式表示的常量称之为直接常量。上述举例都是直接常量。

符号常量:用标识符命名的常量称之为符号常量,就是为上面的直接常量再取一个名字。使用符号常量一是方便理解,提高程序可读性,更重要的是方便程序的后续维护,习惯上符号常量用大写字母和下划线来命名。

比如,可以把3.14取名为PI(即π)。再比如,前边的串口程序采用的波特率是9600,如果用符号常量来进行提前声明的话,那要修改成其它速率的话,就不用在程序中找9600修改了,直接修改声明处就可以了,两种方法举例说明。

  1. const声明。比如在程序开始位置定义一个符号常量BAUD。 定义形式是:const 类型 符号常量名字=常量值; 如const unsigned int BAUD = 9600;/*注意结尾有个分号*/ 就可以在程序中直接把9600全部采用BAUD替换,这样如果要改波特率的话,直接在程序开头位置改一下这个值就可以了。
  2. 用预处理命令#define来完成。 定义形式是:#define 符号常量名 常量值#define BAUD 9600/*注意结尾没有分号*/ 这样定义以后,只要在程序中出现BAUD的话,意思就是完全替代了后边的9600这个数字。之前定义数码管真值表的时候,用了一个code关键字。
unsigned char code LedChar[] = {  //数码管显示字符转换表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};

当时说加了code之后,这个真值表的数据只能被使用,不能被改变,如果直接写LedChar[0] = 1;这样就错了。实际上code这个关键字是51单片机特有的,如果是其它类型的单片机需要写成const unsigned char LedChar[]={},自动保存到FLASH里,而51单片机只用const而不加code的话,这个数组会保存到RAM中,而不会保存到FLAHS中。

整型常量和浮点型常量比较简单,整型直接写数字,十进制如128,前边0x开头的表示是十六进制0x80,浮点型直接写带小数点的数据就可以了。

字符型常量是由一对单引号括起来的单个字符。它分为两种形式,一种是普通字符,一种是转义字符。

普通字符就是那些可以直接书写直接看到的有形的字符,比如阿拉伯数字0~9,英文字符A~z,以及标点符号等。它们都是ASCII码表中的字符,而它们在单片机中都占用一个字节的空间,其值就是对应的ASCII码值。比如‘a’的值是97,‘A’的值是65,‘0’的值是48,如果定义一个变量unsigned char a = ‘a’,那么变量a的值就是97。

除了上述这些字符之外,还有一些特殊字符,它们一些是无形的,像回车符、换行符这些都是看不到的,还有一些像’\’这类字符它们已经有特殊用途了。针对这些特殊符号,为了可以让它们正常进入到程序代码中,C语言就规定了转义字符,它是以反斜杠()开头的特定字符序列,让它们来表示这些特殊字符,比如用\n来代表换行。用一个简单表格来说明一下常用的转义字符的意思,如表11-1所示。

表11-1 常用转义字符及含义

字符形式含义
\n换行
\t横向跳格(相当于Tab)
\v竖向跳格
\b退格
\r光标移到行首
\反斜杠字符‘\’
\’单引号字符
\”双引号字符
\f走纸换页
\0空值

字符串常量是用双引号括起来的字符序列,一般称之字符串。如“a”、“1234”、 “welcome to www.qdkingst.com”等都是字符串常量。字符串常量在内存中按顺序逐个存储字符串中的字符的ASCII码值,并且特别注意,最后还有一个字符‘\0’,‘\0’字符的ASCII码值是0,它是字符串结束标志,在写字符串的时候,这个‘\0’是隐藏的,虽然看不到,但是实际却是存在的。所以“a”就比‘a’多了一个 ‘\0’,“a”的就占了2个字节,而 ‘a’只占一个字节。

还有就是字符串中的空格,也是一个字符,比如“welcome to www.qdkingst.com”一共占了28个字节的空间。其中23个字母,2个‘.’,2个 ‘ ’(空格字符)以及一个‘\0’。

11.2.2字符和字符串数组实例

定义4个数组,通过演示程序对比字符串、字符数组、常量数组的区别。

unsigned char array1[] = "1-Hello!\r\n";
unsigned char array2[] = {'2', '-', 'H', 'e', 'l', 'l', 'o', '!', '\r', '\n'};
unsigned char array3[] = {51, 45, 72, 101, 108, 108, 111, 33, 13, 10};
unsigned char array4[] = "4-Hello!\r\n";

在串口调试助手下,发送十六进制的1、2、3、4,使用字符形式显示,分别往电脑上送这4个数组中对应的那个数组。程序只在起始位置做了区分,其它均没有区别。一方面通过串口调试助手观察,另外通过逻辑分析仪进行对比。

此外还要说明一点,数组1和数组4,数组1发完整的字符串,而数组4仅仅发送数组中的字符,没有发结束符号。串口调试助手用字符形式显示是没有区别的,但是如果改用十六进制显示,会发现数组1比数组4多了一个字节‘\0’的ASCII值00。

#include <reg52.h>

bit cmdArrived = 0; //命令到达标志,即接收到上位机下发的命令
unsigned char cmdIndex = 0; //命令索引,即与上位机约定好的数组编号
unsigned char cntTxd = 0; //串口发送计数器
unsigned char *ptrTxd; //串口发送指针

unsigned char array1[] = "1-Hello!\r\n";
unsigned char array2[] = {'2', '-', 'H', 'e', 'l', 'l', 'o', '!', '\r', '\n'};
unsigned char array3[] = {51, 45, 72, 101, 108, 108, 111, 33, 13, 10};
unsigned char array4[] = "4-Hello!\r\n";

void ConfigUART(unsigned int baud);

void main()
{
EA = 1; //开总中断
ConfigUART(9600); //配置波特率为9600

while (1)
{
if (cmdArrived)
{
cmdArrived = 0;
switch (cmdIndex)
{
case 1:
ptrTxd = array1; //数组1的首地址赋值给发送指针
cntTxd = sizeof(array1); //数组1的长度赋值给发送计数器
TI = 1; //手动方式启动发送中断,处理数据发送
break;
case 2:
ptrTxd = array2;
cntTxd = sizeof(array2);
TI = 1;
break;
case 3:
ptrTxd = array3;
cntTxd = sizeof(array3);
TI = 1;
break;
case 4:
ptrTxd = array4;
cntTxd = sizeof(array4) - 1; //字符串实际长度为数组长度减1
TI = 1;
break;
default:
break;
}
}
}
}
/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud; //计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}
/* UART中断服务函数 */
void InterruptUART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //清零接收中断标志位
cmdIndex = SBUF; //接收到的数据保存到命令索引中
cmdArrived = 1; //设置命令到达标志
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
if (cntTxd > 0) //有待发送数据时,继续发送后续字节
{
SBUF = *ptrTxd; //发出指针指向的数据
cntTxd--; //发送计数器递减
ptrTxd++; //发送指针递增
}
}
}

采用逻辑分析仪将4次收发数据全部抓出来,其中串口助手下发用HEX模式,而接收用文本模式,也就是ASCII码格式显示,逻辑分析仪也是这样配置,如图11-2所示。

图11-2 逻辑分析仪抓取串口字符和字符串信息

从图11-2可以看出,1比2、3、4多了一个结束符,其他内容4组数据是完全一致的。(CR就是\r,LF就是\n)

11.3多.c文件的初步认识

前边课程所涉及到的功能相对简单,程序代码相对较少,用一个文件实现比较方便。随着硬件模块使用的增多,功能复杂度不断增大,程序量变多,往往需要把程序写到多个文件里,方便程序代码的编写、维护和移植。

比如要实现一个比较复杂的串口功能程序,就可以把串口底层的功能函数专门规整到一个单独的uart.c文件内,如串口初始化、串口数据写入、串口数据读出、串口发送接收监控等这些串口基本的底层驱动函数。而把串口读取到的数据分析函数,指令执行等功能函数全部放到main.c中,那main.c文件该如何调用uart.c文件中的函数呢?

C语言中,有一个extern关键字,它有两个基本作用。

  1. 当一个变量的声明不在文件的开头,在它声明之前的函数想要引用的话,则应该用extern进行“外部变量”声明。

    #include <reg52.h>
    sbit LED = P0^0;
    void main()
    {
    extern unsigned int i;
    while(1)
    {
    LED = 0; //点亮小灯
    for(i=0;i<30000;i++); //延时
    LED = 1; //熄灭小灯
    for(i=0;i<30000;i++); //延时
    }
    }
    unsigned int i = 0;
    ... ...

    变量的作用域,是从声明这个变量开始往后所有的程序,如果使用在前,声明在后,就需要用extern这个关键字进行声明。实际开发一般都不会这样做,仅仅是表达一下extern的这个用法。

  2. 在一个工程中,为了方便管理和维护代码,用了多个.c源文件,如果其中一个main.c文件要调用uart.c文件里的变量或者函数的时候,必须得在main.c里边进行外部声明,告诉编译器这个变量或者函数是在其它文件中定义的,可以直接在当前文件中进行调用。

多.c文件工程的编程方式并不复杂。首先新建一个工程,一个工程代表一个完整的单片机程序,只能生成一个hex,但是一个工程可以有很多个.c源文件组成共同参与编译。工程建立好之后,新建文件并且保存取名为main.c文件,再新建一个文件并且保存取名为uart.c文件,下面就可以在两个不同文件中分别编写代码了。当然,在编写程序的过程中,不是说要先把main.c的文件全部写完,再进行uart.c程序的编写,而往往是交互的。

11.4实用串口例程

学串口通信的时候比较注重的是串口底层时序上的操作,例程也都是简单的收发字符或者字符串。在实际应用中,往往串口还要和电脑上的上位机软件进行交互,实现电脑软件发送不同的指令,单片机对应执行不同操作的功能,这就要求组织一个比较合理的通信机制和逻辑关系,用来实现想要的结果。

本节所提供程序的功能是,通过电脑串口调试助手下发三个不同的命令,第一条指令:buzz on可以让蜂鸣器响;第二条指令:buzz off可以让蜂鸣器不响;第三条指令随便输入一个不存在的指令,单片机给电脑串口助手回复一个错误指令。

单片机给电脑发字符串,有多大的数组就发送多少个字节。但是单片机接收数据,接收多少个才应该是一帧完整的数据呢?数据接收起始头在哪里,结束在哪里?这些信息在接收到数据前都是无从得知的,那怎么办呢?

串口编程思路基于这样一种通常的事实:当需要发送一帧(多个字节)数据时,这些数据都是连续不断的发送的,即发送完一个字节后会紧接着发送下一个字节,期间没有间隔或间隔很短,而当这一帧数据都发送完毕后,就会间隔很长一段时间(相对于连续发送时的间隔来讲)不再发送数据,也就是通信总线上会空闲一段较长的时间。于是建立这样一种程序机制:设置一个软件总线空闲定时器,这个定时器在有数据传输时(从单片机接收角度来说就是接收到数据时)清零,而在总线空闲时(也就是没有接收到数据时)时累加,当它累加到一定时间(例程里是30ms)后,就可以认定一帧完整的数据已经传输完毕了,于是告诉其它程序可以来处理数据了,本次的数据处理完后就恢复到初始状态,再准备下一次的接收。那么这个用于判定一帧结束的空闲时间取多少合适呢?它取决于多个条件,并没有一个固定值。这里介绍几个需要考虑的原则:第一,这个时间必须大于一个字节传输时间,很明显单片机接收中断产生是在一个字节接收完毕后,也就是一个时刻点,而其接收过程程序是无从知晓的,因此在至少一个字节传输时间内绝不能认为空闲已经时间达到了。第二,要考虑发送方的系统延时,因为不是所有的发送方都能让数据严格无间隔的发送,由于软件响应、关中断、系统临界区等等操作都会引起延时,所以还得再附加几个到十几个ms的时间。选取30ms是一个折中的经验值,它能适应大部分的波特率(大于1200)和大部分的系统延时(PC机或其它单片机系统)情况。 先把这个程序核心的uart.c文件中的程序贴出来,一点点解析,这实际是项目开发常用的用法,需要熟练掌握。

/*****************************Uart.c文件程序源代码*****************************/
#include <reg52.h>

bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据
bit flagTxd = 0; //单字节发送完成标志,用来替代TXD中断标志位
unsigned char cntRxd = 0; //接收字节计数器
unsigned char xdata bufRxd[64]; //接收字节缓冲区

extern void UartAction(unsigned char *buf, unsigned char len);

/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud; //计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}
/* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */
void UartWrite(unsigned char *buf, unsigned char len)
{
while (len--) //循环发送所有字节
{
flagTxd = 0; //清零发送标志
SBUF = *buf++; //发送一个字节数据
while (!flagTxd); //等待该字节发送完成
}
}
/* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */
unsigned char UartRead(unsigned char *buf, unsigned char len)
{
unsigned char i;

if (len > cntRxd) //指定读取长度大于实际接收到的数据长度时,
{ //读取长度设置为实际接收到的数据长度
len = cntRxd;
}
for (i=0; i<len; i++) //拷贝接收到的数据到接收指针上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收计数器清零

return len; //返回实际读取长度
}
/* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */
void UartRxMonitor(unsigned char ms)
{
static unsigned char cntbkp = 0;
static unsigned char idletmr = 0;

if (cntRxd > 0) //接收计数器大于零时,监控总线空闲时间
{
if (cntbkp != cntRxd) //接收计数器改变,即刚接收到数据时,清零空闲计时
{
cntbkp = cntRxd;
idletmr = 0;
}
else //接收计数器未改变,即总线空闲时,累积空闲时间
{
if (idletmr < 30) //空闲计时小于30ms时,持续累加
{
idletmr += ms;
if (idletmr >= 30) //空闲时间达到30ms时,即判定为一帧接收完毕
{
flagFrame = 1; //设置帧接收完成标志
}
}
}
}
else
{
cntbkp = 0;
}
}
/* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */
void UartDriver()
{
unsigned char len;
unsigned char xdata buf[40];

if (flagFrame) //有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
UartAction(buf, len); //传递数据帧,调用动作执行函数
}
}
/* 串口中断服务函数 */
void InterruptUART() interrupt 4
{
if (RI) //接收到新字节
{
RI = 0; //清零接收中断标志位
if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
{ //保存接收字节,并递增计数器
bufRxd[cntRxd++] = SBUF;
}
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
flagTxd = 1; //设置字节发送完成标志
}
}

可以对照注释和前面的讲解分析下这个uart.c文件,在这里指出其中的两个要点需要多注意。

1、接收数据的处理。在串口中断中,将接收到的字节都存入缓冲区bufRxd中,同时利用另外的定时器中断通过间隔调用UartRxMonitor来监控一帧数据是否接收完毕,判定的原则就是前面介绍的空闲时间,当判定一帧数据结束完毕时,设置flagFrame标志。主循环中可以通过调用UartDriver来检测该标志,并处理接收到的数据。当要处理接收到的数据时,先通过串口读取函数UartRead把接收缓冲区bufRxd中的数据读取出来,然后再对读到的数据进行判断处理。也许有读者会考虑,既然数据都已经接收到bufRxd中了,那直接在这里面用不就行了吗,何必还得再拷贝到另一个地方去呢?设计这种双缓冲的机制,主要是为了提高串口接收到响应效率:首先如果在bufRxd中处理数据,那么这时侯就不能再接收任何数据,因为新接收的数据会破坏原来的数据,造成其不完整和混乱;其次,这个处理过程可能会耗费较长的时间,比如说上位机现在发来一个延时显示的命令,那么在这个延时的过程中都无法去接收新的命令,在上位机看来就是单片机暂时失去响应了。而使用这种双缓冲机制就可以大大改善这个问题,因为数据拷贝所需的时间是相当短的,只要拷贝出去后,bufRxd就可以马上准备去接收新数据了。

2、串口数据写入函数UartWrite,它把数据指针buf指向的数据块连续的由串口发送出去。虽然串口程序启用了中断,但这里的发送功能却没有在中断中完成,而是仍然靠查询发送中断标志flagTxd(因中断函数内必须清零TI,否则中断会重复进入执行,所以另置了一个flagTxd来代替TI)来完成,当然也可以采用先把发送数据拷贝到一个缓冲区中,然后再在中断中发缓冲区数据的方式,但这样一是要耗费额外的内存,二是使程序更复杂。

/*****************************main.c文件程序源代码******************************/
#include <reg52.h>

sbit BUZZ = P1^6; //蜂鸣器控制引脚

unsigned char T0RH = 0; //T0重载值的高字节
unsigned char T0RL = 0; //T0重载值的低字节

void ConfigTimer0(unsigned int ms);
extern void UartDriver();
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartWrite(unsigned char *buf, unsigned char len);

void main()
{
EA = 1; //开总中断
ConfigTimer0(1); //配置T0定时1ms
ConfigUART(9600); //配置波特率为9600

while (1)
{
UartDriver(); //调用串口驱动
}
}
/* 内存比较函数,比较两个指针所指向的内存数据是否相同,ptr1-待比较指针1,ptr2-待比较指针2,len-待比较长度返回值-两段内存数据完全相同时返回1,不同返回0 */
bit CmpMemory(unsigned char *ptr1, unsigned char *ptr2, unsigned char len)
{
while (len--)
{
if (*ptr1++ != *ptr2++) //遇到不相等数据时即刻返回0
{
return 0;
}
}
return 1; //比较完全部长度数据都相等则返回1
}
/* 串口动作函数,根据接收到的命令帧执行响应的动作
buf-接收到的命令帧指针,len-命令帧长度 */
void UartAction(unsigned char *buf, unsigned char len)
{
if (CmpMemory(buf, "buzz on", sizeof("buzz on")-1))
{ //开启蜂鸣器
BUZZ = 0;
}
else if (CmpMemory(buf, "buzz off", sizeof("buzz off")-1))
{ //关闭蜂鸣器
BUZZ = 1;
}
else
{ //非有效命令时,给上机发送“错误命令”的提示
UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
return;
}
//有效命令被执行后,在原命令帧之后添加回车换行符后返回给上位机,表示已执行
buf[len++] = '\r';
buf[len++] = '\n';
UartWrite(buf, len);
}
/* 配置并启动T0,ms-T0定时时间 */
void ConfigTimer0(unsigned int ms)
{
unsigned long tmp; //临时变量

tmp = 11059200 / 12; //定时器计数频率
tmp = (tmp * ms) / 1000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp + 33; //补偿中断响应延时造成的误差
T0RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0为模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
}
/* T0中断服务函数,执行串口接收监控和蜂鸣器驱动 */
void InterruptTimer0() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
UartRxMonitor(1); //串口接收监控
}

重点看CmpMemory函数,这个函数就是比较两段内存数据,通常都是数组中的数据,函数接收两段数据的指针,然后逐个字节比较——if (*ptr1++ != *ptr2++),这行代码既完成了两个指针指向的数据的比较,又在比较完后把两个指针都各自+1,从这里是不是也能领略到一点C语言的简洁高效的魅力呢。这个函数的用处自然就是用来比较接收到的数据和事先放在程序里的命令字符串是否相同,从而找出相符的命令。

将串口调试助手发送和接收显示出来,采用逻辑分析仪将3次单片机发送的数据抓出来做对比,如图11-3所示。(SP为空格)

图11-3 串口收发数据和逻辑分析仪对比图

11.5练习题

  1. 把本章的指针相关内容反复复习,完全掌握指针的基本概念和用法。
  2. 掌握多.c源文件编写代码的方法以及调用其它文件中变量和函数的方法。
  3. 彻底理解实用的串口通信机制程序,能够完全解析明白实用串口通信例程,为今后自己独立编写类似程序打下基础。